Repository navigation
fix: Keep overrides through an in-place rewrite of their files - #479
Closed
kinyoklion wants to merge 31 commits into
Closed
kinyoklion wants to merge 31 commits into
kinyoklion wants to merge 31 commits into
Conversation
Flag overrides, as defined by the OVERRIDE specification, need three pieces below the store: a marker on the flag and segment model that says a definition came from the override store, evaluator marking that follows every definition an evaluation reads, and an indicator on the evaluation reason that reports the marking to callers. The model classes carry the marker as an attribute that is never serialized. `as_override` returns a marked shallow copy and leaves the original unchanged. Only the SDK components that manage override entries set it. The evaluator marks an evaluation as override-affected when the evaluated flag, a prerequisite flag at any depth, or a segment read during clause matching carries the marker. A segment counts when it is read, so a segment that does not match, or that is read through a negated clause, still marks the evaluation. The marking propagates upward only: a prerequisite's own record reflects the definitions its own subtree read, and a plain prerequisite inside a marked evaluation is not marked. An error result is marked when a marked definition was read before the failure, including when the failure was raised while a prerequisite was being evaluated. An evaluation that reads no marked definition keeps the shared precomputed detail instances. `EvaluationReason#override_affected` carries the indicator. It is written to JSON as `overrideAffected` only when true, so the wire format of ordinary evaluations is unchanged. `with_override_affected` follows the `with_big_segments_status` precedent and returns the same instance when nothing changes. Flag overrides are currently experimental and subject to change.
Adds the override layer that the OVERRIDE specification describes: a runtime-mutable collection of flag and segment definitions that takes precedence over LaunchDarkly data at the store read boundary. - `LaunchDarkly::Interfaces::Overrides::OverrideSource` and `OverrideSink` define how a source supplies complete snapshots to the SDK. - `Impl::Overrides::Layer` holds marked entries in an immutable hash that is swapped on update, so the layer holds exactly one snapshot at any instant. - `Impl::Overrides::Overlay` sits at the store read boundary. A read for a key returns the override entry when one exists and the LaunchDarkly entry otherwise. An enumeration is the union with the override entry winning. Initialization status is the base store's alone. - `Impl::Overrides::Sink` applies snapshots and notifies flag change listeners of every flag whose merged-view evaluation may have changed, including flags that depend on an overridden prerequisite or segment. - `DataSystem::ConfigBuilder#overrides` accepts one override source builder. The FDv2 data system builds it at construction, so an invalid configuration raises from `LDClient.new`, starts it before its run loop so the initial load is in effect before the constructor returns, serves reads through the overlay, and stops it when the client closes. Offline clients build no source. - The client consults the override store before the not-initialized short-circuit: an overridden flag is served before LaunchDarkly data arrives, and a flag that is not overridden still returns the client-not-ready default. `all_flags_state` reads through the overlay and, before initialization, returns only the overridden flags. A wrong-type migration result keeps the marking of the evaluation it replaces. - `FileData.make_flag_with_value` gains an off form, which the override source uses so that a value-only override reports the OFF reason kind. The file data sources keep their existing form. The OVERRIDE specification's test vectors run as a spec through the full client stack. The per-evaluation summary marker in the vectors is asserted once the event processor carries it. Flag overrides are currently experimental and subject to change.
… changes The override layer keeps a dependency graph of its entries in each snapshot. The store expands every changed LaunchDarkly item through both its own graph and the layer's, to a fixpoint, so a change to a prerequisite flag or a segment that an override entry references notifies that entry's flag as well. This matches how dependent flags behave for LaunchDarkly data. The store's own dependency graph now keys items by string, the form in which the data model names prerequisites and segments. Changes keyed by symbol, which is what the change set carries, previously reached no dependents at all.
…e source from blocking the client Before initialization, all_flags_state returns a state of overrides alone only when the store is not initialized and the override store holds flags, and it reads those flags from the override store itself. An evaluation does the same for the flag it evaluates. Before, a configured source was enough: an empty override store over a store that held LaunchDarkly data without an init marker produced a valid state of that data, and an evaluation served it. The overlay no longer returns the override entries alone when the base store fails to list its items, so a client whose store fails reports an invalid state and logs the failure, as it does without a source. An exception from the source's start no longer prevents the client from starting: it is logged, the source is stopped, a snapshot it supplied is removed, and the client runs without overrides. Stopping the data system closes the sink before it stops the source, so a snapshot supplied during shutdown is ignored. In offline mode the SDK logs that the configured source is not started.
A source logs and retries a failed load, and raises a configuration error from its builder's build. If start raises anyway, the SDK logs it, stops the source, removes its snapshot, and the client runs without overrides.
When the evaluator raises for a flag that came from the override store, variation_detail and all_flags_state now report the EXCEPTION error reason with override_affected set, from the flag's own marker, as the events already do. all_flags_state also no longer raises NoMethodError when an evaluation raises: the failed flag has no evaluation state, so it reports no prerequisites. That crash predates overrides; it made the exception path of all_flags_state unreachable.
The FeatureStore interface documents its entities as hashes, and a custom store can return a plain hash. The evaluator raises on one, and the rescue then called override? on the hash, so variation_detail and all_flags_state raised NoMethodError instead of returning the EXCEPTION result they returned before overrides. A helper now reads the marker only from an object that has it. A spec also shows that a flag whose evaluation raises after a flag with prerequisites reports no prerequisites: block locals start fresh on each iteration.
…he store fails Overrides only add to the all flags state. When the store cannot list its flags, the state without overrides is invalid and empty, so with overrides it now holds the override store's flags and stays invalid, and the failure is logged as before. A client-side SDK bootstrapped from the state uses its flags whatever $valid says, so the overridden values still reach it.
…ide-affected evaluations Carries the override-affected marking from evaluation into analytics events, as the OVERRIDE specification requires. - `EvalEvent` and `record_eval_event` carry an `override_affected` flag. The client sets it from the evaluation result for the evaluated flag, from each prerequisite record's own marking for prerequisite evaluations, and from the flag's own marker when an evaluation raises. An unknown flag is never marked. - The event dispatcher produces no individual feature event and no debug event for a marked evaluation, whatever the flag's configuration requests. Marked evaluations appear in summary events only. - The summarizer keys counters by version, variation, and the marker, so override-affected and other evaluations of the same flag, variation, and version accumulate into separate counters. The summary event writes `overrideAffected: true` on the marked counters only, like the existing `unknown` marker. - `all_flags_state` presents an override-affected flag with `trackEvents` false, `trackReason` false, and no `debugEventsUntilDate`. The flag, its value, its version, and its reason stay in the state. The specification vectors now also check the summary marker the client hands to the event processor for each evaluation. Flag overrides are currently experimental and subject to change.
…raises When the evaluator raises in all_flags_state there is no evaluation result, so the state kept trackEvents, trackReason, and debugEventsUntilDate for an overridden flag. The flag's own override marker now decides, as it does for the reason and for the event of a single evaluation.
…jects A custom store can return a flag as a plain hash, as the FeatureStore interface documents. When its evaluation raised, the error event and the all_flags_state tracking fallback called override? on the hash and raised NoMethodError. Both now use the helper that reads the marker only from an object that has it, so such a failure is unmarked and keeps the flag's tracking, as before overrides.
Adds `LaunchDarkly::Integrations::FileData.override_source`, the file-based override source that the OVERRIDE specification defines, for use with `LaunchDarkly::DataSystem::ConfigBuilder#overrides`. The source reads one or more JSON or YAML files in the file data source document format (`flags`, `flagValues`, `segments`) and supplies complete snapshots to the SDK's override store. Options: `paths` (required, in precedence order), `duplicate_keys_handling` (`:fail`, the default, or `:ignore`, which keeps the first file's entry), `change_detection` (`:polling`, the default, or `:watching` through the optional listen gem), and `poll_interval` (default 1 second, raised to the 1 second minimum with a warning). Invalid options, including an unknown option key, raise `ArgumentError` when the client is created. Behavior comes from the shared file data code: a configured file that does not exist contributes no overrides, so a file can be created later and deleting a file removes its overrides. A file that exists but cannot be read or parsed fails that reload and keeps the last good overrides, with a logged failure and a retry. A rewrite with identical content is not reapplied. The initial load completes before the client constructor returns. A `flagValues` entry expands into a flag that is off and serves the value, so the evaluation reports the OFF reason kind. Every applied change is logged at Info with the overrides in effect and what each file supplied. The reloader's log lines for this source carry a FileOverrideSource prefix. Flag overrides are currently experimental and subject to change.
…ntries as duplicate keys A path listed more than once would make every entry of that file a duplicate of itself: under the default fail handling the load failed, and under ignore the first listing won by accident. The builder now drops the repeats, keeps the first occurrence in its place in the order, and logs a warning once at build time. This is the behavior the Node SDK has.
… SDKs A flagValues entry becomes a flag without client-side availability, so all_flags_state with client_side_only leaves it out even when the flag it overrides is client-side. A full definition with clientSide is the way to override such a flag. A spec pins both.
The test service advertises the flag-overrides capability and accepts the overrides block of the SDK configuration: filePaths, duplicateKeysHandling, changeDetection, and pollIntervalMs map to the options of LaunchDarkly::Integrations::FileData.override_source on the FDv2 data system builder. Overrides without a data system configuration are an error. The listen gem joins the contract test bundle so that the watching change detection mode can run. The service port can be set with the PORT environment variable and still defaults to 9000.
Applications and editors rewrite files in place: they truncate the file, then write the new content. A reload in between found the override file empty and applied it as a document with no entries, so the client briefly served LaunchDarkly data for every overridden flag. The parser is now chosen by the file name: a name that ends in .yaml or .yml, in any case, is YAML, and any other file is JSON. An empty or white space JSON file is not a JSON document and fails to parse, so the last good overrides stay. Files are read as UTF-8 without a byte order mark. A load that finds a file empty while the applied data has content from it puts the file off: it keeps the current data and arms the retry without reporting a failure. The file is put off for one retry delay from the first load that found it empty; a write while it is empty (a new rewrite, seen through the modification time) starts the delay again, up to five retry delays in all. A file that contributed no content to the applied data, such as an empty placeholder, is parsed at once.
Member
Author
|
bugbot review |
…olding loads
A file whose content is in effect and that a load finds with no content
is held back. Four cases still let a rewrite through or held too long:
- A read of a YAML file that sees only a comment or "---" is not white
space, but the parser reads it as nothing, and it was applied as no
entries. Content that the YAML parser reads as a null now counts as no
content. "{}" is a document and is applied at once.
- The five retry delay cap was kept per file and restarted whenever the
file was read with content, so files rewritten in turn held every load.
The cap now applies to the run of loads that hold files back, which any
load that holds nothing back ends, including a failed one.
- The delay check had a tenth of slack, so another load could apply a file
a little before its delay was over. The check is now exact, and the
retry of a hold is armed for the rest of the first held file's delay, or
of the run's limit, plus a tenth of the retry delay.
- The file's state is now its modification time and size, read through a
symbolic link with the poller's helper.
The tests of the delays run on a controlled clock with synchronous loads.
Member
Author
|
bugbot review |
Three cases of the hold of an empty file went wrong: - The clock was read once per load, so a slow read of an earlier file made a later file's emptiness look older and shortened its delay. The clock is now read after each file's own read, and again for the retry. - The bound of five retry delays applied to the run of holding loads, and a failed load ended the run, so a file kept written while empty was held without end while another file failed now and then. A file that reached the bound also let through another file's ordinary rewrite. Each file now has its own bound, counted from the first load that held it back. It goes on across reads that find the file with content and across failed loads, and starts over when data is applied or when the file stayed empty for the whole delay. The retry of a hold comes when the first held file's delay or bound is over, plus a tenth of the retry delay, and the retry logs that it reads the files again because a file was found empty.
Member
Author
|
bugbot review |
… back Two cases of the per-file bound held data back too long or let a rewrite through: - A file that stayed empty for the whole delay started its own bound over while another file kept every load holding, so its next truncation got a fresh bound. Two files kept empty and truncated again in turn were held without end. - A file held back during a long failure of another file kept its bound, so a later load that fixed the other file while this one was being rewritten applied this one empty. A file's bound now starts over only at a load that holds nothing back, whether it applies, is skipped as unchanged, or fails, for every file that load read. A file that the load did not get to keeps its bound.
Member
Author
|
bugbot review |
A file saved in place with an error, then fixed in place more than five retry delays later, had the fix applied empty: the load that failed on the file returned before the file was counted among the files it read, so the file kept the bound of the hold before the error. A load now records every file it gets to and does not hold back, including the file whose read or parse fails and a missing file, and a load that holds nothing back starts the bound over for all of them. A file after the failing one keeps its bound. A read that finds a file with content, even content that fails to parse, or finds it missing, ends the file's empty state, so its next emptying starts a new delay.
Member
Author
|
bugbot review |
A file with a UTF-16 or UTF-32 byte order mark, which Windows PowerShell 5.1 writes with `>`, made the read raise an error that no load rescued. At startup the data system stopped the source; at runtime the worker thread ended, so no change was ever loaded again. - Files are read as bytes, a UTF-8 byte order mark is removed, and the content is taken as UTF-8. Content that is not valid UTF-8, in either format, fails to parse like any other invalid content: the failure is logged and the data stays. A UTF-16 or UTF-32 file is reported as such. - An error that a load does not expect is logged and retried like a failed load, and the worker rescues errors inside its loop, so that it keeps running. - YAML with only white space, including a tab, a vertical tab or a form feed, is a document with no entries, as documented; the YAML parser rejected some of it. - Tests record the delay of the retry that is armed, not the one that was asked for, so that the arming paths are checked.
…resence A file configured after a failing file kept the bound of a hold from before the failure: every load stopped at the failing file, so the later file was never read and its bound never started over. When a deploy fixed the failing file while rewriting the later one in place, the later one's truncation was applied empty. A load now reads and classifies every configured file even after a read or parse failure, and fails with the first failure after them, before it holds a file back or merges. A load that holds nothing back then starts the bound over for every file. The skip-unchanged digest now covers each path and whether it is present, not only the bytes of the files that are present. The format comes from the name, so the same bytes moved from a JSON file to a YAML file parse differently and are applied.
Member
Author
|
bugbot review |
Member
Author
|
bugbot review |
Member
Author
|
bugbot review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 6f10871. Configure here.
kinyoklion
force-pushed
the
rlamb/overrides-ruby-contract-tests
branch
from
November 3, 2026 23:58
8b8a337 to
7cffc44
Compare
This was referenced Nov 4, 2026
Member
Author
|
The changes of this PR moved into the stack:
#450, #451, #452 and #454 were rebased with their own changes unchanged. The tree of the new #454 head, 7cffc44, is identical to this PR's head, 6f10871 (tree 49524067). This branch stays in place. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Applications and editors rewrite override files in place: they truncate the file and then write the new content. A reload that reads the file in between finds it empty. On this stack, that reload applied the empty file as a document with no entries, so the override source briefly removed every override, and the client served LaunchDarkly data for every overridden flag until the next reload. A slow rewrite (truncate, pause 300 ms, write) made a client serve
override-a -> ld-value -> override-b, for JSON and YAML files, in polling and watching modes.The OVERRIDE spec's retry requirement exists to keep "a partially written or briefly invalid file from emptying the layer" (section 1.3). This change keeps the overrides through such a rewrite. It changes only the shared file data code under
FileOverrideSource(Impl::FileData); the file data sources (FileData.data_sourceandFileData.data_source_v2) do not use it and are unchanged.The format comes from the file name, and an empty JSON file is invalid
Every override file was parsed with the YAML parser, which also reads JSON, so an empty file was always a YAML document with no entries. The parser is now chosen by the file name, as the spec recommends (sdk-specs #272): a name that ends in
.yamlor.yml, in any case, is YAML, and any other file, including one with no extension, is JSON. Whether an empty file is valid is then up to the format, like the rest of the content:the file has no content, which is not a JSON document, and the last good overrides stay, as for any other content that cannot be parsed. To remove every override, write{}or delete the file.Other effects of parsing JSON files as JSON, for a file named
.jsonor any other name that is not YAML, including one with no extension:#comment, or a trailing comma.1e3is1000.0, where the YAML parser read it as the string"1e3".jsongem: 3.x rejects both, 2.x accepts them (2.13 and later warn about a duplicate key and keep the last value). The SDK parses all of its other JSON the same way, and theoverride_sourcedocs say so.For both formats, files are read as bytes and taken as UTF-8, without a UTF-8 byte order mark, whatever the process's default encoding. Content that is not valid UTF-8 fails to parse, like any other invalid content: the failure is logged once and the data stays. On MRI the YAML parser accepted a Latin-1 file, and the strings it produced later made event serialization fail. A file with a UTF-16 or UTF-32 byte order mark, which Windows PowerShell 5.1 writes with
>, is reported asencoded in UTF-16 or UTF-32; data files must be UTF-8; it is not decoded.Since the format comes from the name, the same bytes can parse differently in another file, so the skip-unchanged digest now covers each configured path, whether its file is present, and its content; before, it covered only the content of the files that were present, and the same bytes moved from a JSON file to a YAML file were skipped as unchanged.
I checked the rest of the file data code for other rules that decide by content where the name should, and found none. A missing file is handled before parsing, so it never reaches the JSON rule. The
skip_missing_pathsdocumentation now says a missing file "contributes no data" instead of calling it "a file with no content".A file that is emptied is held back before it is applied
With the JSON rule alone, a JSON file that is empty for a moment fails to parse and is retried, and that failure is logged at error level. A YAML file can legitimately be empty, so its parser cannot tell an emptied file from one in the middle of a rewrite. So the reloader now holds back a file that a load finds with no content while the data currently applied has content from that file. No content means empty, white space only, or YAML that the parser reads as a null: a read in the middle of a rewrite can see only a leading comment or
---. A YAML document such as{}is content and is applied at once; so isflagValues:alone, which the parser reads as a mapping (see "Not changed" below). Content with no JSON document is only parsed when the file is not held back, and then fails.A load that holds a file back keeps the current overrides, logs at debug level that it will read the file again, and arms a retry, whatever the format. It reports no failure: it does not log at error level, call
on_error, or change which failure the reloader last reported.reload_nowreturns false for it, as for a failure.For each file, the reloader keeps whether it contributed content to the data currently applied (updated when a load is applied or skipped as unchanged), and, once a load has found the file with no content: since when it has been so without being written, its state then (modification time and size, through any symbolic link, with the poller's helper), and when a load first held it back.
FileOverrideSourcealways retries.Process.clock_gettime(Process::CLOCK_MONOTONIC), in floating-point seconds, which does not overflow. Tests inject a controlled clock for the hold; scheduling always uses the system's clock.A rewrite is covered when the gap between truncating and writing is shorter than the retry delay. A writer that pauses longer gets the empty file applied (YAML) or reported (JSON), as before. On a file system that records modification times in whole seconds, a second truncation within the same second is not seen as a new rewrite.
Errors that a load does not expect
An error that a load does not expect is logged as
Unexpected error in file data reloaderand retried like a failed load, and the worker thread rescues errors inside its loop, so that no error stops reloading for good. An earlier version of this branch read files in a way that raised on a UTF-16 byte order mark, and that error ended the worker at runtime, or made the data system stop the source at startup.Documentation
FileData.override_sourcesays only what an application needs: a file whose name ends in.yamlor.ymlis YAML and any other file is JSON; files must be UTF-8; whether a JSON file may contain comments or repeated keys depends on the application's version of thejsongem; files can be rewritten in place, and the source keeps the overrides in effect while it finds a file empty during a rewrite; to remove a file's overrides, delete the file or write{}; an empty JSON file is not a valid document; and replacing a YAML file in one operation avoids briefly applying a partly written file (see below). The hold's delay and bound, other files waiting, and the retries are described in theReloadercomments and in this description, not in the public docs. TheReloaderandDocument.parsecomments describe the rules above.Not changed: YAML that is cut off
A read that overlaps a write can see the first part of a YAML file. A part with only a comment or
---is held back (above), but most other parts are valid YAML documents: a cut between two entries gives fewer entries, a cut inside a value gives the value cut short (on-valuebecomeson-v, oron, which istrue), and a cut right afterflagValues:gives a mapping whoseflagValuesis null, a document with no entries. The part is applied until the next reload. JSON that is cut off fails to parse. This predates this change. Telling a part from a whole file needs a content rule this change does not add, so the docs recommend replacing a YAML file in one operation.Testing
DocumentandFileDatatests: the format of each file name (.yaml,.yml, any case; JSON for.json, no extension,.txtand.yaml.json); an empty or white-space JSON document fails with the message above, including for a file with no extension; empty YAML (empty, white space, a comment,---,~) is a document with no entries; a JSONnulland a YAMLfalseare not objects; JSON numbers and trailing content; a YAML document in a JSON file fails; a file with a byte order mark reads, and one with only a byte order mark is empty.holding back a file, on a controlled clock):---is held back, and{}is applied at once, in YAML and in JSON.a file that is found empty), with a 1.5 s retry delay where a test must finish a rewrite before the retry, waiting on the debug line:reload_nowreturns false for a hold; a reloader stopped during the read neither logs nor arms a retry; with retries off an emptied file is applied right away; at the initial load an empty YAML file has no entries and an empty JSON file fails.Poller.observereads the state of the file a symbolic link leads to.InPlaceWrites.rewriteinspec/in_place_writes.rb):override-a -> override-band never the LaunchDarkly value, for JSON and YAML.retries a failed reload after the retry delay and recovers when the file is fixedno longer accepts an empty application as the recovery.expected: ["override-a", "override-b"], got: ["override-a", "ld-value", "override-b"]), 60 reloader tests (the new ones that pass cover behavior the base already has, such as a placeholder, a file deleted and created empty, retries off, a stop during the read, an empty YAML file at the initial load, and a parse failure of another file), 21 document tests (many of them becauseDocument.parsenow takes the format), and the new poller test.bundle exec rspec specon Ruby 3.2.3: 1502 examples, 1 failure; on JRuby 9.4.6 with--tag ~flaky, as CI runs it: 1469 examples, 1 failure, 1 pending. The failure iscan be automatically loaded by Bundler.require, which needs write access to the system gem directory on this machine and fails the same way on the base of this branch. Earlier JRuby runs also failed one of two FDv2 data system timing tests now and then; this PR does not touch them.bundle exec rubocop: no offenses.flag overridessuite of sdk-test-harness (3.2.0-alpha.13, built from #515, whose override files are rewritten in place and have no extension) passes against this branch's test service: 79 tests ran, all passed.Place in the stack
This PR is based on #454, the top of the Ruby overrides stack, and changes code that #449 (the parser and the reloader) and #453 (the override source docs) add. It can be merged on top of the stack or folded into those PRs.
Revisions
---seen mid-write) is held back; the five-delay cap applies to the run of holding loads, which any load that holds nothing back ends, instead of to each file; the delay check is exact and the retry of a hold is armed for the rest of the delay plus a tenth; the file's state is read through symbolic links with the poller's helper; the delay tests run on a controlled clock. With the first commit alone, the new end-to-end test with a leading YAML comment supplies empty snapshots in both modes, and a load halfway through the delay pushes the application from about 2.2 s to 3 s..yamlor.yml. Ninth commit: the publicoverride_sourcedocs are cut down to what an application needs; the details of the hold stay in the internal comments and here.Opened from termag
Note
Overview
Fixes a gap where in-place file rewrites could briefly clear file-based flag overrides because a reload read an empty or partial file and applied it as “no entries.”
Parsing and I/O now pick JSON vs YAML from the filename (
.yaml/.ymlvs everything else), parse JSON withJSON.parse, enforce UTF-8 (reject UTF-16/32 BOMs, strip UTF-8 BOM on read), and treat blank JSON as invalid while blank YAML maps to an empty document (Document::NONE).Reloaderadds hold-back logic: if a file that previously contributed data is read empty/whitespace-only (or YAML-only-null), the load keeps the last good overrides, logs at debug, and schedules a retry after a settle window (with per-file delay, max hold bound, and symlink-aware file state viaPoller.observe). Skip-unchanged digests now include path and presence, not just raw bytes.Public
override_sourcedocs describe format-by-extension, UTF-8, and in-place rewrite behavior; specs addInPlaceWritesand broad reloader/integration coverage.Reviewed by Cursor Bugbot for commit 6f10871. Bugbot is set up for automated code reviews on this repo. Configure here.